﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using SharedGenomics.Annotations;
using SharedGenomics.Workbench.Annotations;

namespace SharedGenomics.Workbench.Tests.Annotations
{
    [TestClass]
    public class AnnotationRepositoryTests
    {
        private AnnotationRepository repository;

        [TestInitialize]
        public void Setup()
        {
            //create graph edges
            var edges = new List<AnnotationKeyDependencyEdge>
            {
                new AnnotationKeyDependencyEdge(AnnotationKey.SnpName, AnnotationKey.GoogleSearch),
                new AnnotationKeyDependencyEdge(AnnotationKey.SnpName, AnnotationKey.EntrezGeneId),
                new AnnotationKeyDependencyEdge(AnnotationKey.EntrezGeneId, AnnotationKey.Publications),
                new AnnotationKeyDependencyEdge(AnnotationKey.EntrezGeneId, AnnotationKey.PubMedSearch)
            };

            AnnotationKeyDependencyGraph graph = new AnnotationKeyDependencyGraph(edges);
            this.repository = new AnnotationRepository(graph);
        }

        [TestMethod]
        public void ShouldRetrieveSnpNameForAnnotation()
        {
            this.repository.Add(AnnotationKey.SnpName, "rs001", null);
            this.repository.Add(AnnotationKey.GoogleSearch, "googleAnnotation", new AnnotationValue(AnnotationKey.SnpName, "rs001"));

            string snpName = (string)this.repository.GetTargetForAnnotation(AnnotationKey.GoogleSearch, "googleAnnotation");
            Assert.AreEqual("rs001", snpName);
        }

        [TestMethod]
        public void ShouldReturnNullIfParentValueNull()
        {
            this.repository.Add(AnnotationKey.SnpName, "rs001", null);
            this.repository.Add(AnnotationKey.GoogleSearch, "googleAnnotation", null);

            //cannot find snp name for annotation since there is no parent data to link the two
            Assert.IsNull(this.repository.GetTargetForAnnotation(AnnotationKey.GoogleSearch, "googleAnnotation"));
        }

        [TestMethod]
        public void ShouldReturnNullIfRootItemNotFound()
        {
            string snpName = (string)this.repository.GetTargetForAnnotation(AnnotationKey.GoogleSearch, "notFound");
            Assert.IsNull(snpName);
        }

        [TestMethod]
        public void ShouldReturnNullIfKeyNotFound()
        {
            string snpName = (string)this.repository.GetTargetForAnnotation(AnnotationKey.UcscBrowser, "browserAnnotation");
            Assert.IsNull(snpName);
        }

        [TestMethod]
        public void ShouldReturnNullIfKeyNull()
        {
            Assert.IsNull(this.repository.GetTargetForAnnotation(null, "abc"));
        }

        [TestMethod]
        public void ShouldReturnNullIfValueNull()
        {
            Assert.IsNull(this.repository.GetTargetForAnnotation(AnnotationKey.GoogleSearch, null));
        }

        [TestMethod]
        public void ShouldRetrievePublicationForSnpName()
        {
            //add all intermediate values to the repository
            this.repository.Add(AnnotationKey.SnpName, "rs001", null);
            this.repository.Add(AnnotationKey.EntrezGeneId, 1, new AnnotationValue(AnnotationKey.SnpName, "rs001"));
            this.repository.Add(AnnotationKey.Publications, "publications", new AnnotationValue(AnnotationKey.EntrezGeneId, 1));

            string publications = (string)this.repository.GetAnnotationForTarget(AnnotationKey.Publications, "rs001");
            Assert.AreEqual("publications", publications);
        }

        [TestMethod]
        public void ShouldNotRetrieveOrphanedPublication()
        {
            //add snp name and publication to repository
            //note there is no complete path from snp name to publication so the publication cannot be found
            this.repository.Add(AnnotationKey.SnpName, "rs001", null);
            this.repository.Add(AnnotationKey.Publications, "publications", new AnnotationValue(AnnotationKey.EntrezGeneId, 1));

            string publications = (string)this.repository.GetAnnotationForTarget(AnnotationKey.Publications, "rs001");
            Assert.IsNull(publications);
        }

        [TestMethod]
        public void ShouldReturnNullForMissingPublication()
        {
            //add all intermediate values to the repository
            this.repository.Add(AnnotationKey.SnpName, "rs001", null);
            this.repository.Add(AnnotationKey.EntrezGeneId, 1, new AnnotationValue(AnnotationKey.SnpName, "rs001"));

            //no publication so should return null
            string publications = (string)this.repository.GetAnnotationForTarget(AnnotationKey.Publications, "rs001");
            Assert.IsNull(publications);
        }

        [TestMethod]
        public void ShouldAllowConcurrentReadAndWrite()
        {
            //create 100 threads each of which will insert 10000 annotations and then fetch them again
            Action<object> threadAction = (si) => {
                int startIndex = ((int)si * 10000);
                var annotations = Enumerable.Range(startIndex, 10000).Select(i => String.Format("Annotation{0}", i));

                foreach(string a in annotations)
                    this.repository.Add(AnnotationKey.SnpName, a, null);

                foreach (string a in annotations)
                    Assert.IsNotNull(this.repository.GetTargetForAnnotation(AnnotationKey.SnpName, a));
            };

            List<Thread> threadList = Enumerable.Range(0, 100).Select(i => new Thread(new ParameterizedThreadStart(threadAction))).ToList();

            //start all threads then wait for them to finish
            for (int i = 0; i < threadList.Count; i++)
            {
                threadList[i].Start(i);
            }

            foreach (var thread in threadList)
            {
                thread.Join();
            }
        }

        [TestMethod]
        public void ShouldAllowConcurrentLookup()
        {
            //create 10 threads each of which will insert 10000 'chains' of annotations:
            //snp name -> entrez gene id -> pubmed
            Action<object> threadAction = si =>
            {
                int startIndex = ((int)si * 10000);

                foreach (int anIdx in Enumerable.Range(startIndex, 10000))
                {
                    this.repository.Add(AnnotationKey.SnpName, "rs" + anIdx, null);
                    this.repository.Add(AnnotationKey.EntrezGeneId, anIdx, new AnnotationValue(AnnotationKey.SnpName, "rs" + anIdx));
                    this.repository.Add(AnnotationKey.PubMedSearch, "pubMed" + anIdx, new AnnotationValue(AnnotationKey.EntrezGeneId, anIdx));
                }

                foreach (int anIdx in Enumerable.Range(startIndex, 10000))
                {
                    Assert.AreEqual("pubMed" + anIdx, this.repository.GetAnnotationForTarget(AnnotationKey.PubMedSearch, "rs" + anIdx));
                    Assert.AreEqual("rs" + anIdx, this.repository.GetTargetForAnnotation(AnnotationKey.PubMedSearch, "pubMed" + anIdx));
                }
            };

            List<Thread> threadList = Enumerable.Range(0, 10).Select(i => new Thread(new ParameterizedThreadStart(threadAction))).ToList();

            //start all threads then wait for them to finish
            for (int i = 0; i < threadList.Count; i++)
            {
                threadList[i].Start(i);
            }

            foreach (var thread in threadList)
            {
                thread.Join();
            }
        }
    }
}
